luci-app-attendedsysupgrade: backport several bugfixes
authorEric Fahlgren <[email protected]>
Sat, 8 Feb 2025 14:10:05 +0000 (06:10 -0800)
committerPaul Donald <[email protected]>
Tue, 18 Feb 2025 18:41:52 +0000 (12:41 -0600)
Fix bug in applyPackageChanges, it was using the installed version as the
target, resulting in no changes being applied.

Signed-off-by: Eric Fahlgren <[email protected]>
(cherry picked from commit 2e9ffc6628d91065326cc1e16ea15f9f8f4f00a0)

applications/luci-app-attendedsysupgrade/htdocs/luci-static/resources/view/attendedsysupgrade/overview.js

index 06bc1bb068079b6640e91600dbb0c37bbab639cb..145d75faf08a28e48e4e2fd1381a263e8e129470 100644 (file)
@@ -9,17 +9,17 @@
 'require dom';
 'require fs';
 
-let callPackagelist = rpc.declare({
+const callPackagelist = rpc.declare({
        object: 'rpc-sys',
        method: 'packagelist',
 });
 
-let callSystemBoard = rpc.declare({
+const callSystemBoard = rpc.declare({
        object: 'system',
        method: 'board',
 });
 
-let callUpgradeStart = rpc.declare({
+const callUpgradeStart = rpc.declare({
        object: 'rpc-sys',
        method: 'upgrade_start',
        params: ['keep'],
@@ -60,39 +60,72 @@ function get_revision_count(revision) {
 
 return view.extend({
        steps: {
-               init: [10, _('Received build request')],
-               download_imagebuilder: [20, _('Downloading ImageBuilder archive')],
-               unpack_imagebuilder: [40, _('Setting Up ImageBuilder')],
-               calculate_packages_hash: [60, _('Validate package selection')],
-               building_image: [80, _('Generating firmware image')],
+               init:                    [  0, _('Received build request')],
+               container_setup:         [ 10, _('Setting up ImageBuilder')],
+               validate_revision:       [ 20, _('Validating revision')],
+               validate_manifest:       [ 30, _('Validating package selection')],
+               calculate_packages_hash: [ 40, _('Calculating package hash')],
+               building_image:          [ 50, _('Generating firmware image')],
+               signing_images:          [ 95, _('Signing images')],
+               done:                    [100, _('Completed generating firmware image')],
+               failed:                  [100, _('Failed to generate firmware image')],
+
+               /* Obsolete status values, retained for backward compatibility. */
+               download_imagebuilder:   [ 20, _('Downloading ImageBuilder archive')],
+               unpack_imagebuilder:     [ 40, _('Setting Up ImageBuilder')],
        },
 
-       data: {
-               url: '',
-               revision: '',
-               advanced_mode: 0,
-               rebuilder: [],
-               sha256_unsigned: '',
-       },
+       request_hash: '',
+       sha256_unsigned: '',
+
+       applyPackageChanges: async function(package_info) {
+               let { url, target, version, packages } = package_info;
+
+               const overview_url = `${url}/api/v1/overview`;
+               const revision_url = `${url}/api/v1/revision/${version}/${target}`;
 
-       firmware: {
-               profile: '',
-               target: '',
-               version: '',
-               packages: [],
-               diff_packages: true,
-               filesystem: '',
+               let changes, target_revision;
+
+               await Promise.all([
+                       request.get(overview_url).then(
+                               (response) => {
+                                       let json = response.json();
+                                       changes = json.branches[get_branch(version)].package_changes;
+                               },
+                               (failed) => {
+                                       ui.addNotification(null, E('p', _(`Get overview failed ${failed}`)));
+                               }
+                       ),
+                       request.get(revision_url).then(
+                               (response) => {
+                                       target_revision = get_revision_count(response.json().revision);
+                               },
+                               (failed) => {
+                                       ui.addNotification(null, E('p', _(`Get revision failed ${failed}`)));
+                               }
+                       ),
+               ]);
+
+               for (const change of changes) {
+                       let idx = packages.indexOf(change.source);
+                       if (idx >= 0 && change.revision <= target_revision) {
+                               if (change.target)
+                                       packages[idx] = change.target;
+                               else
+                                       packages.splice(idx, 1);
+                       }
+               }
+               return packages;
        },
 
-       selectImage: function (images) {
-               let firmware = this.firmware;
-               let data = this.data;
+       selectImage: function (images, data, firmware) {
                var filesystemFilter = function(e) {
                        return (e.filesystem == firmware.filesystem);
                }
                var typeFilter = function(e) {
-                       if (firmware.target.indexOf("x86") != -1) {
-                               // x86 images can be combined-efi (EFI) or combined (BIOS)
+                       let efi_targets = ['armsr', 'loongarch', 'x86'];
+                       let efi_capable = efi_targets.some((tgt) => firmware.target.startsWith(tgt));
+                       if (efi_capable) {
                                if (data.efi) {
                                        return (e.type == 'combined-efi');
                                } else {
@@ -105,13 +138,13 @@ return view.extend({
                return images.filter(filesystemFilter).filter(typeFilter)[0];
        },
 
-       handle200: function (response) {
+       handle200: function (response, content, data, firmware) {
                response = response.json();
-               let image = this.selectImage(response.images);
+               let image = this.selectImage(response.images, data, firmware);
 
                if (image.name != undefined) {
-                       this.data.sha256_unsigned = image.sha256_unsigned;
-                       let sysupgrade_url = `${this.data.url}/store/${response.bin_dir}/${image.name}`;
+                       this.sha256_unsigned = image.sha256_unsigned;
+                       let sysupgrade_url = `${data.url}/store/${response.bin_dir}/${image.name}`;
 
                        let keep = E('input', { type: 'checkbox' });
                        keep.checked = true;
@@ -123,7 +156,7 @@ return view.extend({
                                image.sha256,
                        ];
 
-                       if (this.data.advanced_mode == 1) {
+                       if (data.advanced_mode == 1) {
                                fields.push(
                                        _('Profile'),
                                        response.id,
@@ -142,7 +175,7 @@ return view.extend({
                                '',
                                E('a', { href: sysupgrade_url }, _('Download firmware image'))
                        );
-                       if (this.data.rebuilder) {
+                       if (data.rebuilder) {
                                fields.push(_('Rebuilds'), E('div', { id: 'rebuilder_status' }));
                        }
 
@@ -185,15 +218,15 @@ return view.extend({
                        ];
 
                        ui.showModal(_('Successfully created firmware image'), modal_body);
-                       if (this.data.rebuilder) {
-                               this.handleRebuilder();
+                       if (data.rebuilder) {
+                               this.handleRebuilder(content, data, firmware);
                        }
                }
        },
 
        handle202: function (response) {
                response = response.json();
-               this.data.request_hash = response.request_hash;
+               this.request_hash = response.request_hash;
 
                if ('queue_position' in response) {
                        ui.showModal(_('Queued...'), [
@@ -219,8 +252,14 @@ return view.extend({
                }
        },
 
-       handleError: function (response) {
+       handleError: function (response, data, firmware) {
                response = response.json();
+               const request_data = {
+                       ...data,
+                       request_hash: this.request_hash,
+                       sha256_unsigned: this.sha256_unsigned,
+                       ...firmware
+               };
                let body = [
                        E('p', {}, _('Server response: %s').format(response.detail)),
                        E(
@@ -229,7 +268,7 @@ return view.extend({
                                _('Please report the error message and request')
                        ),
                        E('p', {}, _('Request Data:')),
-                       E('pre', {}, JSON.stringify({ ...this.data, ...this.firmware }, null, 4)),
+                       E('pre', {}, JSON.stringify({ ...request_data }, null, 4)),
                ];
 
                if (response.stdout) {
@@ -251,23 +290,23 @@ return view.extend({
                ui.showModal(_('Error building the firmware image'), body);
        },
 
-       handleRequest: function (server, main) {
+       handleRequest: function (server, main, content, data, firmware) {
                let request_url = `${server}/api/v1/build`;
                let method = 'POST';
-               let content = this.firmware;
+               let local_content = content;
 
                /**
                 * If `request_hash` is available use a GET request instead of
                 * sending the entire object.
                 */
-               if (this.data.request_hash && main == true) {
-                       request_url += `/${this.data.request_hash}`;
-                       content = {};
+               if (this.request_hash && main == true) {
+                       request_url += `/${this.request_hash}`;
+                       local_content = {};
                        method = 'GET';
                }
 
                request
-                       .request(request_url, { method: method, content: content })
+                       .request(request_url, { method: method, content: local_content })
                        .then((response) => {
                                switch (response.status) {
                                        case 202:
@@ -285,13 +324,13 @@ return view.extend({
                                        case 200:
                                                if (main == true) {
                                                        poll.remove(this.pollFn);
-                                                       this.handle200(response);
+                                                       this.handle200(response, content, data, firmware);
                                                } else {
                                                        poll.remove(this.rebuilder_polls[server]);
                                                        response = response.json();
                                                        let view = document.getElementById(server);
-                                                       let image = this.selectImage(response.images);
-                                                       if (image.sha256_unsigned == this.data.sha256_unsigned) {
+                                                       let image = this.selectImage(response.images, data, firmware);
+                                                       if (image.sha256_unsigned == this.sha256_unsigned) {
                                                                view.innerText = '✅ %s'.format(server);
                                                        } else {
                                                                view.innerHTML = `⚠️ ${server} (<a href="${server}/store/${
@@ -305,7 +344,7 @@ return view.extend({
                                        case 500: // build failed
                                                if (main == true) {
                                                        poll.remove(this.pollFn);
-                                                       this.handleError(response);
+                                                       this.handleError(response, data, firmware);
                                                        break;
                                                } else {
                                                        poll.remove(this.rebuilder_polls[server]);
@@ -317,14 +356,17 @@ return view.extend({
                        });
        },
 
-       handleRebuilder: function () {
+       handleRebuilder: function (content, data, firmware) {
                this.rebuilder_polls = {};
-               for (let rebuilder of this.data.rebuilder) {
+               for (let rebuilder of data.rebuilder) {
                        this.rebuilder_polls[rebuilder] = L.bind(
                                this.handleRequest,
                                this,
                                rebuilder,
-                               false
+                               false,
+                               content,
+                               data,
+                               firmware
                        );
                        poll.add(this.rebuilder_polls[rebuilder], 5);
                        document.getElementById(
@@ -401,14 +443,14 @@ return view.extend({
                        });
        },
 
-       handleCheck: function () {
-               let { url, revision } = this.data;
-               let { version, target } = this.firmware;
+       handleCheck: function (data, firmware) {
+               this.request_hash = '';
+               let { url, revision, advanced_mode, branch } = data;
+               let { version, target, profile, packages } = firmware;
                let candidates = [];
-               let request_url = `${url}/api/overview`;
-               if (version.endsWith('SNAPSHOT')) {
-                       request_url = `${url}/api/v1/revision/${version}/${target}`;
-               }
+
+               const endpoint = version.endsWith('SNAPSHOT') ? `revision/${version}/${target}` : 'overview';
+               const request_url = `${url}/api/v1/${endpoint}`;
 
                ui.showModal(_('Searching...'), [
                        E(
@@ -448,6 +490,9 @@ return view.extend({
                        } else {
                                const latest = response.json().latest;
 
+                               // ensure order: newest to oldest release
+                               latest.sort().reverse();
+
                                for (let remote_version of latest) {
                                        let remote_branch = get_branch(remote_version);
 
@@ -457,24 +502,21 @@ return view.extend({
                                        }
 
                                        // skip branch upgrades outside the advanced mode
-                                       if (
-                                               this.data.branch != remote_branch &&
-                                               this.data.advanced_mode == 0
-                                       ) {
+                                       if (branch != remote_branch && advanced_mode == 0) {
                                                continue;
                                        }
 
                                        candidates.unshift([remote_version, null]);
 
                                        // don't offer branches older than the current
-                                       if (this.data.branch == remote_branch) {
+                                       if (branch == remote_branch) {
                                                break;
                                        }
                                }
                        }
 
                        // allow to re-install running firmware in advanced mode
-                       if (this.data.advanced_mode == 1) {
+                       if (advanced_mode == 1) {
                                candidates.unshift([version, revision]);
                        }
 
@@ -483,9 +525,9 @@ return view.extend({
 
                                let mapdata = {
                                        request: {
-                                               profile: this.firmware.profile,
+                                               profile,
                                                version: candidates[0][0],
-                                               packages: Object.keys(this.firmware.packages).sort(),
+                                               packages: Object.keys(packages).sort(),
                                        },
                                };
 
@@ -517,7 +559,7 @@ return view.extend({
                                        }
                                }
 
-                               if (this.data.advanced_mode == 1) {
+                               if (advanced_mode == 1) {
                                        o = s.option(form.Value, 'profile', _('Board Name / Profile'));
                                        o = s.option(form.DynamicList, 'packages', _('Packages'));
                                }
@@ -527,8 +569,8 @@ return view.extend({
                                                E(
                                                        'p',
                                                        _('Currently running: %s - %s').format(
-                                                               this.firmware.version,
-                                                               this.data.revision
+                                                               version,
+                                                               revision
                                                        )
                                                ),
                                                form_rendered,
@@ -541,14 +583,24 @@ return view.extend({
                                                                        class: 'btn cbi-button cbi-button-positive important',
                                                                        click: ui.createHandlerFn(this, function () {
                                                                                map.save().then(() => {
-                                                                                       this.firmware.packages = mapdata.request.packages;
-                                                                                       this.firmware.version = mapdata.request.version;
-                                                                                       this.firmware.profile = mapdata.request.profile;
-                                                                                       this.pollFn = L.bind(function () {
-                                                                                               this.handleRequest(this.data.url, true);
-                                                                                       }, this);
-                                                                                       poll.add(this.pollFn, 5);
-                                                                                       poll.start();
+                                                                                       this.applyPackageChanges({
+                                                                                               url,
+                                                                                               target,
+                                                                                               version:  mapdata.request.version,
+                                                                                               packages: mapdata.request.packages,
+                                                                                       }).then((packages) => {
+                                                                                               const content = {
+                                                                                                       ...firmware,
+                                                                                                       packages: packages,
+                                                                                                       version: mapdata.request.version,
+                                                                                                       profile: mapdata.request.profile
+                                                                                               };
+                                                                                               this.pollFn = L.bind(function () {
+                                                                                                       this.handleRequest(url, true, content, data, firmware);
+                                                                                               }, this);
+                                                                                               poll.add(this.pollFn, 5);
+                                                                                               poll.start();
+                                                                                       });
                                                                                });
                                                                        }),
                                                                },
@@ -574,44 +626,43 @@ return view.extend({
                });
        },
 
-       load: function () {
-               return Promise.all([
+       load: async function () {
+               const promises = await Promise.all([
                        L.resolveDefault(callPackagelist(), {}),
                        L.resolveDefault(callSystemBoard(), {}),
                        L.resolveDefault(fs.stat('/sys/firmware/efi'), null),
                        uci.load('attendedsysupgrade'),
                ]);
+               const data = {
+                       url: uci.get_first('attendedsysupgrade', 'server', 'url'),
+                       branch: get_branch(promises[1].release.version),
+                       revision: promises[1].release.revision,
+                       efi: promises[2],
+                       advanced_mode: uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0,
+                       rebuilder: uci.get_first('attendedsysupgrade', 'server', 'rebuilder')
+               };
+               const firmware = {
+                       client: 'luci/' + promises[0].packages['luci-app-attendedsysupgrade'],
+                       packages: promises[0].packages,
+                       profile: promises[1].board_name,
+                       target: promises[1].release.target,
+                       version: promises[1].release.version,
+                       diff_packages: true,
+                       filesystem: promises[1].rootfs_type
+               };
+               return [data, firmware];
        },
 
        render: function (response) {
-               this.firmware.client =
-                       'luci/' + response[0].packages['luci-app-attendedsysupgrade'];
-               this.firmware.packages = response[0].packages;
-
-               this.firmware.profile = response[1].board_name;
-               this.firmware.target = response[1].release.target;
-               this.firmware.version = response[1].release.version;
-               this.data.branch = get_branch(response[1].release.version);
-               this.firmware.filesystem = response[1].rootfs_type;
-               this.data.revision = response[1].release.revision;
-
-               this.data.efi = response[2];
-
-               this.data.url = uci.get_first('attendedsysupgrade', 'server', 'url');
-               this.data.advanced_mode =
-                       uci.get_first('attendedsysupgrade', 'client', 'advanced_mode') || 0;
-               this.data.rebuilder = uci.get_first(
-                       'attendedsysupgrade',
-                       'server',
-                       'rebuilder'
-               );
+               const data = response[0];
+               const firmware = response[1];
 
                return E('p', [
                        E('h2', _('Attended Sysupgrade')),
                        E(
                                'p',
                                _(
-                                       'The attended sysupgrade service allows to easily upgrade vanilla and custom firmware images.'
+                                       'The attended sysupgrade service allows to upgrade vanilla and custom firmware images easily.'
                                )
                        ),
                        E(
@@ -623,15 +674,15 @@ return view.extend({
                        E(
                                'p',
                                _('Currently running: %s - %s').format(
-                                       this.firmware.version,
-                                       this.data.revision
+                                       firmware.version,
+                                       data.revision
                                )
                        ),
                        E(
                                'button',
                                {
                                        class: 'btn cbi-button cbi-button-positive important',
-                                       click: ui.createHandlerFn(this, this.handleCheck),
+                                       click: ui.createHandlerFn(this, this.handleCheck, data, firmware),
                                },
                                _('Search for firmware upgrade')
                        ),